/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2005-2007 Philippe Durand <draekz@gmail.com>
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Xml.Serialization;

using Anculus.Core;

using Galaxium.Core;
using Galaxium.Protocol.Msn.Soap;
using Galaxium.Protocol.Msn.Soap.BodyParts;

namespace Galaxium.Protocol.Msn
{
	public sealed class MsnSession : AbstractSession
	{
		public event EventHandler<SessionEventArgs> Usurped;
		
		const int _xfrInterval = 8;
		
		NSConnection _connection;
		bool _disposed;
		
		ContactCollection _contacts;
		GroupCollection _groups;
		
		OIMService _oimService;
		OIMStoreService _oimStoreService;
		PassportService _ppService;
		SharingService _sharingService;
		ABService _abService;
		StoreService _sstoreService;
		
		internal FindMembershipResult _memberships;
		internal ABFindAllResult _addressBook;
		
		MsnClientConfig _clientConfig = new MsnClientConfig ();
		
		internal List<SBConnection> _switchboards = new List<SBConnection> ();
		Dictionary<string, RequestSecurityTokenResponse> _securityTokens = new Dictionary<string, RequestSecurityTokenResponse> ();
		
		static Dictionary<MsnSession, List<SBConnection>> _awaitingXfr = new Dictionary<MsnSession, List<SBConnection>> ();
		private uint _xfrTimerId = 0;
		internal DateTime _lastXfrCompleted = DateTime.Now;

		public new MsnAccount Account
		{
			get { return base.Account as MsnAccount; }
		}
		
		internal OIMService OIMService
		{
			get { return _oimService; }
		}
		
		internal OIMStoreService OIMStoreService
		{
			get { return _oimStoreService; }
		}
		
		internal PassportService PassportService
		{
			get { return _ppService; }
		}
		
		internal SharingService SharingService
		{
			get { return _sharingService; }
		}
		
		internal ABService ABService
		{
			get { return _abService; }
		}
		
		internal StoreService StoreService
		{
			get { return _sstoreService; }
		}
		
		internal Dictionary<string, RequestSecurityTokenResponse> SecurityTokens
		{
			get { return _securityTokens; }
		}
		
		public MsnClientConfig ClientConfig
		{
			get { return _clientConfig; }
		}
		
		public MsnSession (MsnAccount account)
			: base (account)
		{
			_lastXfrCompleted = _lastXfrCompleted.Subtract(new TimeSpan(0, 0, _xfrInterval));
			
			_contacts = new ContactCollection ();
			_groups = new GroupCollection ();
			
			Conversations = new MsnConversationManager ();
			
			IGroup emptyGroup = new MsnGroup (this, "0", "Other Contacts", true);
			
			_groups.Add (emptyGroup);
			
			account.Presence = account.InitialPresence;
			
			_oimService = new OIMService (this);
			_oimStoreService = new OIMStoreService (this);
			_ppService = new PassportService (this);
			_sharingService = new SharingService (this);
			_abService = new ABService (this);
			_sstoreService = new StoreService (this);
			
			foreach (RequestSecurityTokenResponse token in (Account.Cache as MsnAccountCache).GetSecurityTokens ())
				_securityTokens.Add (token.AppliesTo.EndpointReference.Address, token);
			
			// If we have a stored notification server address/port, use it
			// NSConnection will fall back to the dispatch server if necessary
			
			//TODO: get the proxy settings from the account
			
			if (!string.IsNullOrEmpty (account.NotificationServerHostname))
				_connection = new NSConnection (this, new MsnNSConnectionInfo (account.NotificationServerHostname, account.NotificationServerPort, account.UseHTTP));
			else
				_connection = new NSConnection (this, new MsnNSConnectionInfo (account.UseHTTP));
			
			_connection.Closed += ConnectionClosed;
			_connection.ErrorOccurred += ConnectionErrorOccurred;
		}

		protected override void Dispose (bool disposing)
		{
			if (!_disposed)
			{
				_connection.Closed -= ConnectionClosed;
				_connection.ErrorOccurred -= ConnectionErrorOccurred;
				
				if (disposing && _connection != null)
					_connection.Dispose ();
				
				_oimService.Dispose ();
				_oimStoreService.Dispose ();
				_ppService.Dispose ();
				_sharingService.Dispose ();
				_abService.Dispose ();
				_sstoreService.Dispose ();
			}
			
			_disposed = true;
		}
		
		void ConnectionErrorOccurred (object sender, ConnectionErrorEventArgs args)
		{
			OnErrorOccurred (new ErrorEventArgs (this, args.ErrorIdentifier, args.Description));
		}
		
		void ConnectionClosed (object sender, ConnectionEventArgs args)
		{
			if (!_connection.Reconnecting)
			{
				CloseSwitchboards ();
				OnErrorOccurred (new ErrorEventArgs (this, "Closed", "The connection was lost"));
			}
		}
		
		void CloseSwitchboards ()
		{
			while (_switchboards.Count > 0)
				_switchboards[0].Disconnect();
		}

		public NSConnection Connection
		{
			get { return _connection; }
		}
		
		public override GroupCollection GroupCollection
		{
			get { return this._groups; }
		}

		public override ContactCollection ContactCollection
		{
			get { return this._contacts; }
		}

		public IEnumerable<IGroup> Groups
		{
			get { return this._groups; }
		}
		
		public MsnProtocolVersion Protocol
		{
			get { return _connection.Protocol; }
		}

		public override void Connect ()
		{
			_connection.Connect ();
			OnConnected (new SessionEventArgs (this));
		}

		public override void Disconnect ()
		{
			if (_xfrTimerId != 0)
			{
				TimerUtility.RemoveCallback(_xfrTimerId);
				_xfrTimerId = 0;
			}
			
			foreach (IConversation conversation in Conversations)
				conversation.Close();
			
			_oimService.Abort ();
			_oimStoreService.Abort ();
			_ppService.Abort ();
			_sharingService.Abort ();
			_abService.Abort ();
			_sstoreService.Abort ();
			
			_connection.Disconnect ();
			
			OnDisconnected (new SessionEventArgs (this));
		}
		
		public bool AddContactToGroups (out string error, string passport, params string [] guids)
		{
			return _connection.AddContactToGroups (out error, passport, guids);
		}
		
		public bool AddContactWithGroups (out string error, string passport, string alias, bool block, params string [] guids)
		{
			return _connection.AddContactWithGroups (out error, passport, alias, block, guids);
		}

		public bool AddContact (out string error, string passport, string alias, bool block)
		{
			return _connection.AddContact (out error, passport, alias, block);
		}
		
		public bool MoveContactToGroup (out string error, string passport, string fromGuid, string toGuid)
		{
			return _connection.MoveContactToGroup (out error, passport, fromGuid, toGuid);
		}
		
		public bool RemoveContactFromGroups (out string error, string passport, params string [] guids)
		{
			return _connection.RemoveContactFromGroups (out error, passport, guids);
		}
		
		public bool RemoveContact (out string error, string uid)
		{
			return _connection.RemoveContact (out error, uid);
		}

		public bool AddGroup (out string error, string name)
		{
			return _connection.AddGroup (out error, name);
		}
		
		public bool RemoveGroup (out string error, string guid, bool clear)
		{
			return _connection.RemoveGroup (out error, guid, clear);
		}
		
		public bool RenameGroup (out string error, string guid, string name)
		{
			return _connection.RenameGroup (out error, guid, name);
		}
		
		public override IFileTransfer SendFile (IContact contact, string fileName)
		{
			ThrowUtility.ThrowIfNull ("contact", contact);
			ThrowUtility.ThrowIfEmpty ("fileName", fileName);
			
			return new MsnFileTransfer (this, contact, fileName);
		}
		
		public IEnumerable<MsnGroup> GetGroupsForContact (MsnContact contact)
		{
			lock (_groups)
			{
				foreach (MsnGroup group in _groups)
				{
					if (group.Contains (contact))
						yield return group;
				}
			}
		}
		
		public void EmitUsurped (SessionEventArgs args)
		{
			if (Usurped != null)
				Usurped (this, args);
		}
		
		public override void SetPresence (BasePresence presence)
		{
			IPresence msnpresence = MsnPresence.Get (presence);
			
			if (msnpresence != null)
				Account.Presence = msnpresence;
			else
				Log.Warn("Cannot handle BasePresence {0}", presence);
		}
		
		public override IEntity FindEntity (string uid)
		{
			string networkName = string.Empty;
			
			if (uid.StartsWith ("<"))
			{
				int colon = uid.IndexOf (":");
				
				networkName = uid.Substring (1, colon - 1);
				uid = uid.Substring (colon + 1, uid.Length - colon - 2);
			}
			
			return FindEntity (uid, MsnNetworkUtility.FromName (networkName));
		}
		
		public IMsnEntity FindEntity (string uid, Network network)
		{
			if ((_account.UniqueIdentifier == uid) && ((network & Network.WindowsLive) == network))
				return _account as MsnAccount;
			
			return FindContact (uid, network);
		}
		
		public MsnContact FindContact (string uid)
		{
			string networkName = string.Empty;
			
			if (uid.StartsWith ("<"))
			{
				int colon = uid.IndexOf (":");
				
				networkName = uid.Substring (1, colon - 1);
				uid = uid.Substring (colon + 1, uid.Length - colon - 2);
			}
			
			return FindContact (uid, MsnNetworkUtility.FromName (networkName));
		}
		
		public MsnContact FindContact (string uid, Network network)
		{
			lock (_contacts)
			{
				foreach (MsnContact contact in _contacts)
				{
					if (!contact.UniqueIdentifier.Equals (uid, StringComparison.InvariantCultureIgnoreCase))
						continue;
					
					if ((contact.Network & network) != network)
						continue;
					
					return contact;
				}
				
				MsnContact newContact = new MsnContact (this, uid, network);
				
				try
				{
					_contacts.Add (newContact);
				}
				catch (Exception ex)
				{
					Log.Error (ex, "Error adding contact {0} ({1} Network)", uid, network);
				}
				
				return newContact;
			}
		}
		
		internal void UpdateMemberships (FindMembershipResult result)
		{
			if (_memberships == null)
				_memberships = result;
			else
			{
				_memberships.OwnerNamespace = result.OwnerNamespace.Clone ();
				
				foreach (Service svc in result.Services)
				{
					if (_memberships.Services[svc.Info.Handle.Type.Name] != null)
					{
						Service s = _memberships.Services[svc.Info.Handle.Type.Name];
						
						s.Info = svc.Info.Clone ();
						s.LastChange = svc.LastChange;
						s.LastChangeSpecified = svc.LastChangeSpecified;
						
						foreach (Membership mship in svc.Memberships)
						{
							if (s.Memberships[mship.MemberRole] != null)
							{
								foreach (Member mbr in mship.Members)
									s.Memberships[mship.MemberRole].Members[mbr.MembershipId] = mbr.Clone ();
							}
							else
								s.Memberships[mship.MemberRole] = mship.Clone ();
						}
					}
					else
						_memberships.Services.Add (svc.Clone ());
				}
			}
			
			List<KeyValuePair<Membership, Member>> removals = new List<KeyValuePair<Membership, Member>> ();
			
			foreach (Service svc in _memberships.Services)
			{
				foreach (Membership mship in svc.Memberships)
				{
					foreach (Member mbr in mship.Members)
					{
						if (mbr.Deleted)
							removals.Add (new KeyValuePair<Membership, Member> (mship, mbr));
					}
				}
			}
			
			foreach (KeyValuePair<Membership, Member> pair in removals)
			{
				Log.Debug ("Member {0} {1} deleted", pair.Value.MembershipId, (pair.Value is PassportMember) ? (pair.Value as PassportMember).PassportName : pair.Value.ToString ());
				
				pair.Key.Members.Remove (pair.Value);
			}
			
			(Account.Cache as MsnAccountCache).SaveCachedMembership (_memberships, _memberships.OwnerNamespace.LastChangeString);
		}
		
		internal void UpdateAddressBook (ABFindAllResult result)
		{
			if (_addressBook == null)
				_addressBook = result;
			else
			{
				_addressBook.AB.Info = result.AB.Info;
				
				foreach (ABContact contact in result.Contacts)
				{
					if (_addressBook.Contacts[contact.Id] != null)
					{
						ABContact c = _addressBook.Contacts[contact.Id];
						
						if (contact.InfoSpecified)
							c.Info = contact.Info.Clone ();
						if (contact.PropertiesChangedSpecified)
							c.PropertiesChanged = contact.PropertiesChanged;
					}
					else
						_addressBook.Contacts.Add (contact.Clone ());
				}
				
				foreach (ABGroup group in result.Groups)
				{
					if (_addressBook.Groups[group.Id] != null)
					{
						ABGroup g = _addressBook.Groups[group.Id];
						
						if (group.InfoSpecified)
							g.Info = group.Info.Clone ();
						if (group.PropertiesChangedSpecified)
							g.PropertiesChanged = group.PropertiesChanged;
					}
					else
						_addressBook.Groups.Add (group.Clone ());
				}
			}
			
			(Account.Cache as MsnAccountCache).SaveCachedAddressBook (_addressBook, _addressBook.AB.LastChangeString);
		}
		
		internal void UpdateContacts ()
		{
			lock (_groups)
			{
				// First, add all groups to the GroupCollection
				foreach (ABGroup group in _addressBook.Groups)
				{
					MsnGroup existing = null;
					
					foreach (MsnGroup g in _groups)
					{
						if (g.UniqueIdentifier == group.Id.ToString ())
						{
							existing = g;
							break;
						}
					}
					
					if (existing == null)
					{
						existing = new MsnGroup (this, group.Id.ToString (), group.Info.Name);
						_groups.Add (existing);
					}
					else
						existing.Name = group.Info.Name;
				}
			}
			
			lock (_contacts)
			{
				// Next, add contacts from the Messenger membership list
				
				if (_memberships.Services["Messenger"] != null)
				{
					foreach (Membership membership in _memberships.Services["Messenger"].Memberships)
					{
						MsnListType listType = GetListType (membership.MemberRole);
						
						if (listType == MsnListType.Unknown)
							continue;
						
						//Log.Debug ("Membership: {0}", membership.MemberRole);
						
						foreach (Member member in membership.Members)
						{
							string email = string.Empty;
							Network network = Network.Unknown;
							
							try
							{
								switch (member.Type)
								{
								case MemberType.Passport:
									email = ((PassportMember)member).PassportName;
									network = Network.WindowsLive;
									break;
								case MemberType.Email:
									email = ((EmailMember)member).Email;
									network = GetNetwork ((EmailMember)member);
									break;
								case MemberType.Phone:
									// FIXME: Is this really how you parse a phone type member? or do we?
									//email = ((PhoneMember)member).Phone;
									//network = Network.Telephone;
									break;
								case MemberType.Role:
									// How is this parsed, and what is a role member anyway?
									break;
									
								default:
									Log.Debug ("  Unknown type: "+member.Type);
									break;
								}
								
								if (network == Network.Unknown)
									throw new ApplicationException ("Unknown network");
								
								//Log.Debug ("  Member {0}: {1}, {2}", network, email, member.Type);
								
								MsnContact contact = FindContact (email, network) as MsnContact;
								
								if (contact == null)
									throw new ApplicationException ("Member could not be found");
								
								contact.ListType |= listType;
								contact.MembershipId = member.MembershipId;
							}
							catch (Exception ex)
							{
								Log.Error (ex, "Error loading member");
								
								try
								{
									XmlSerializer serializer = new XmlSerializer (typeof (Member));
									StringWriter writer = new StringWriter ();
									
									serializer.Serialize (writer, member);
									
									Log.Debug ("Member:\n{0}", writer.ToString ());
									
									writer.Close ();
								}
								catch (Exception ex2)
								{
									Log.Error (ex2, "Unable to serialize member");
								}
							}
						}
					}
				}
				
				// Finally, add contacts from the address book to the forward list
				// and get information such as names from address book
				foreach (ABContact contact in _addressBook.Contacts)
				{
					try
					{
						if (contact.Info.Type == ContactType.Me)
						{
							(_account as MsnAccount).SetDisplayName (contact.Info.DisplayName);
							(_account as MsnAccount)._contactID = contact.Info.CID;
							
							continue;
						}
						
						string email = contact.Info.PassportName;
						Network network = Network.Unknown;
						
						if (string.IsNullOrEmpty (email))
						{
							// The contact does not have a passport account
							// Check if they have a messenger enabled email address
							// eg. Yahoo users
							
							foreach (ContactEmail cEmail in contact.Info.Emails)
							{
								if (cEmail.IsMessengerEnabled)
								{
									email = cEmail.Email;
									network = (Network)cEmail.Capability;
									break;
								}
							}
						}
						else
							network = Network.WindowsLive;
						
						if (network == Network.Unknown)
						{
							Log.Warn ("Unknown network, ignoring contact '{0}'", email);
							continue;
						}
						
						MsnContact msnContact = FindContact (email, network) as MsnContact;
						
						if (msnContact == null)
							throw new ApplicationException ("Unable to find contact");
						
						msnContact.SetDisplayName (contact.Info.DisplayName, false);
						msnContact.Guid = contact.Id.ToString ();
						msnContact.ListType |= MsnListType.Forward;
						
						// Add the contact to the correct groups
						// Add to the default group if none are given in the address book
						if (contact.Info.GroupIds.Count < 1)
						{
							if (!_groups.GetGroup ("0").Contains (msnContact))
								_groups.GetGroup ("0").Add (msnContact);
						}
						else
						{
							foreach (Guid groupId in contact.Info.GroupIds)
							{
								if (!_groups.GetGroup (groupId.ToString ()).Contains (msnContact))
									_groups.GetGroup (groupId.ToString ()).Add (msnContact);
							}
						}
					}
					catch (Exception ex)
					{
						Log.Error (ex, "Error loading contact");
						
						try
						{
							XmlSerializer serializer = new XmlSerializer (typeof (ABContact));
							StringWriter writer = new StringWriter ();
							
							serializer.Serialize (writer, contact);
							
							Log.Debug ("Contact:\n{0}", writer.ToString ());
							
							writer.Close ();
						}
						catch (Exception ex2)
						{
							Log.Error (ex2, "Unable to serialize contact");
						}
					}
				}
				
				// Somehow contacts can sometimes be on both the allow & block lists
				// In this case we remove them from allow
				foreach (MsnContact contact in ContactCollection)
				{
					if (((contact.ListType & MsnListType.Allowed) == MsnListType.Allowed) && 
					    ((contact.ListType & MsnListType.Blocked)) == MsnListType.Blocked)
					{
						Log.Debug ("Contact {0} in {1} lists!", contact.UniqueIdentifier, contact.ListType);
						contact.ListType &= ~MsnListType.Allowed;
					}
				}
			}
		}
		
		Network GetNetwork (EmailMember member)
		{
			string buddyType = member.Annotations["MSN.IM.BuddyType"];
			int val;
			
			if (buddyType.Contains (":") && int.TryParse (buddyType.Substring (0, buddyType.IndexOf (":")), out val))
				return (Network)val;
			
			return Network.Unknown;
		}
		
		MsnListType GetListType (MemberRole role)
		{
			switch (role)
			{
			case MemberRole.Allow:
				return MsnListType.Allowed;
			case MemberRole.Block:
				return MsnListType.Blocked;
			case MemberRole.Pending:
				return MsnListType.Pending;
			case MemberRole.Reverse:
				return MsnListType.Reverse;
			default:
				return MsnListType.Unknown;
			}
		}
		
		internal void UpdateSecurityTokens (RequestSecurityTokenResponseCollection tokens)
		{
			foreach (RequestSecurityTokenResponse token in tokens)
				UpdateSecurityToken (token);
		}
		
		internal void UpdateSecurityToken (RequestSecurityTokenResponse token)
		{
			_securityTokens[token.AppliesTo.EndpointReference.Address] = token;
			(Account.Cache as MsnAccountCache).SaveSecurityToken (token);
		}
		
		internal void RequireSecurityTokens (ExceptionDelegate callback, params string[] domains)
		{
			bool allOk = true;
			
			// How long should be require the token to last before we request a new one?
			foreach (string domain in domains)
				allOk &= _securityTokens.ContainsKey (domain) && _securityTokens[domain].LifeTime.Expires.ToLocalTime() > DateTime.Now.AddMinutes (2);
			
			if (allOk)
			{
				Log.Debug ("Required security tokens all OK, no request needed");
				
				if (callback != null)
					callback (null);
				
				return;
			}
			
			Log.Debug ("Security tokens expired or not present, requesting new ones");
			
			RequestMultipleSecurityTokens requests = new RequestMultipleSecurityTokens ();
			requests.Add (SecurityToken.PassportTb, SecurityToken.GetPolicy (SecurityToken.PassportTb));
			
			foreach (string domain in domains)
				requests.Add (domain, SecurityToken.GetPolicy (domain));
			
			if (callback == null)
			{
				RequestSecurityTokenResponseCollection responses = PassportService.RequestMultipleSecurityTokens (requests);
				UpdateSecurityTokens (responses);
			}
			else
			{
				PassportService.BeginRequestMultipleSecurityTokens (requests, delegate (IAsyncResult asyncResult)
				{
					Exception ex = null;
					
					try
					{
						RequestSecurityTokenResponseCollection responses = PassportService.EndRequestMultipleSecurityTokens (asyncResult);
						UpdateSecurityTokens (responses);
					}
					catch (Exception e)
					{
						ex = e;
					}
					
					callback (ex);
				}, null);
			}
		}
		
		internal void RequireSecurityTokens (params string[] tokens)
		{
			RequireSecurityTokens (null, tokens);
		}
		
		// Find a switchboard with the given contacts
		public SBConnection FindSwitchboard (params MsnContact[] contacts)
		{
			StringBuilder uids = new StringBuilder ();
			
			if (CoreUtility.Debug)
			{
				bool first = true;
				foreach (MsnContact contact in contacts) {
					if (first)
						first = false;
					else
						uids.Append (", " );

					uids.Append (contact.UniqueIdentifier);
				}
			}
			
			lock (_switchboards)
			{
				foreach (SBConnection sb in _switchboards)
				{
					// Check if the switchboard has the contacts we want
					bool match = false;
					
					// If the switchboard doesn't have the same amount of contacts
					// we know it's not what we want & can skip checking each contact
					if (sb.Contacts.Count == contacts.Length)
					{
						match = true;
						
						foreach (MsnContact contact in contacts)
							match &= sb.Contacts.Contains (contact);
					}
					
					// It doesn't, but maybe it's not yet established & it will have the
					// contacts we want when it is, so check if it will be inviting them
					if ((!match) && (!sb._authenticated) && (sb._inviteContacts.Length == contacts.Length))
					{
						match = true;
						
						foreach (MsnContact contact in contacts)
						{
							bool sbHasContact = false;
							
							foreach (MsnContact sbContact in sb._inviteContacts)
							{
								// Check if the switchboards contact is the same as the contact we want
								if (sbContact == contact)
								{
									sbHasContact = true;
									break;
								}
							}
							
							// If the switchboard has all the contacts we want, it's a match
							// otherwise, match will be false
							match &= sbHasContact;
						}
					}
				
					// We found a switchboard that matches what we're looking for!
					if (match)
					{
						Log.Debug ("Found switchboard with {0}", uids.ToString ());
						
						return sb;
					}
				}
			}
			
			Log.Debug ("No switchboard found with {0}", uids.ToString ());
			
			return null;
		}
		
		public SBConnection FindSwitchboard (string id)
		{
			lock (_switchboards)
			{
				foreach (SBConnection sb in _switchboards)
				{
					if (sb.ID == id)
						return sb;
				}
			}
			
			return null;
		}
		
		internal void RequestSwitchboard (SBConnection connection, bool priority)
		{
			if (connection.RequestState == XFRRequestState.Incomplete ||
				connection.RequestState == XFRRequestState.Requested)
			{
				lock (_awaitingXfr)
				{
					if (!_awaitingXfr.ContainsKey (this))
						_awaitingXfr.Add (this, new List<SBConnection> ());
					
					TimeSpan span = DateTime.Now.Subtract (_lastXfrCompleted);
					
					if (_awaitingXfr[this].Count == 0 && span.TotalSeconds > _xfrInterval)
					{
						_lastXfrCompleted = DateTime.Now;
						connection.RequestState = XFRRequestState.Requested;
						Connection.Send (new XFRCommand (this), connection.OnXFRResponseReceived);
						return;
					}
					else
					{
						// Now for safety, lets make sure we dont insert the same connection more than once.
						if (_awaitingXfr[this].Contains(connection))
						{
							// We already have this connection in the queue, if its priority, move it up.
							if (priority)
							{
								_awaitingXfr[this].Remove(connection);
								_awaitingXfr[this].Insert(0, connection);
							}
							else
								return;
						}
						else
						{
							// Now we have to worry about priority queueing. if the priority is set to true
							// We need to put this specific switchboard request ahead of the others.
							if (priority)
							{
								// We have to put this connection as priority.
								_awaitingXfr[this].Insert (0, connection);
							}
							else
							{
								// We have to put this at the end of the line.
								_awaitingXfr[this].Add (connection);
							}
							
							connection.RequestState = XFRRequestState.Requested;
						}
						
						if (_xfrTimerId == 0)
						{
							int time = (int)(_xfrInterval - span.TotalSeconds) * 1000;
							
							if (time < 1)
							{
								Log.Debug ("Time to delay the XFR callback is less than 1!");
								time = 1000;
							}
							
							_xfrTimerId = TimerUtility.RequestInfiniteCallback(OnXFRTimerElapsed, time);
						}
					}
				}
			}
		}
		
		private void OnXFRTimerElapsed ()
		{
			lock (_awaitingXfr)
			{
				if (_awaitingXfr.ContainsKey (this))
				{
					SBConnection connection = _awaitingXfr[this][0];
					_awaitingXfr[this].Remove (connection);
					
					if (connection != null)
						Connection.Send (new XFRCommand (this), connection.OnXFRResponseReceived);
					
					if (_awaitingXfr[this].Count < 1)
					{
						if (_xfrTimerId != 0)
						{
							TimerUtility.RemoveCallback(_xfrTimerId);
							_xfrTimerId = 0;
						}
					}
				}
			}
		}
		
		// Find a switchboard with the given contacts, and create one if we don't already have one
		public SBConnection GetSwitchboard (bool priority, params MsnContact[] contacts)
		{
			SBConnection sb = FindSwitchboard (contacts);
			
			if (sb == null)
			{
				// There's no switchboard already created with the contacts we want
				// So we need to create a new one
				
				lock (_switchboards)
				{
					sb = new SBConnection (this, priority, contacts);
					
					sb.Closed += delegate
					{
						if (sb.Reconnecting)
							return;
						
						Log.Debug ("Switchboard {0} closed", sb.ID);
						
						lock (_switchboards)
							_switchboards.Remove (sb);
					};
					
					Log.Debug ("Switchboard {0} created", sb.ID);
					_switchboards.Add (sb);
				}
			}
			else
			{
				// We have a swithcboard, but it may not have completed its request.
				if (sb.RequestState == XFRRequestState.Incomplete)
					RequestSwitchboard (sb, priority);
			}
			
			return sb;
		}
		
		public override bool InContactList (IContact contact)
		{
			return base.InContactList (contact) && (contact as MsnContact).IsInList (MsnListType.Forward);
		}
		
		
	}
}
